from base64 import b32decode, b32encode
from functools import cached_property
import os
import pathlib
import re

from django.core.exceptions import SuspiciousFileOperation
from django.core.files.storage import FileSystemStorage
from django.core.files.utils import validate_file_name
from django.core.files.move import file_move_safe
from django.core.files import File, locks
from django.utils._os import safe_join

from liceum38 import settings


# Увеличивает номер в скобках в самом конце файла или создаёт этот номер
def increase_file_name(file_root: str):
    if re.match(r'.+_\d+_$', file_root):
        number = int(file_root[file_root.rfind('_', 0, -1)+1:-1])
        return file_root[:file_root.rfind('_', 0, -1)+1]+str(number+1)+'_'
    return file_root + '_1_'


class MyStorage(FileSystemStorage):
    
    @cached_property
    def base_location(self):
        return self._value_or_setting(self._location, safe_join(settings.MEDIA_ROOT, 'project_files'))
    
    def _open(self, name, mode="rb"):
        return File(open(self.path(name), mode))
    
    def get_available_name(self, name, max_length=None):
        # Этот код я просто скопировал из исходника
        """
                Return a filename that's free on the target storage system and
                available for new content to be written to.
                """
        name = str(name).replace("\\", "/")
        dir_name, file_name = os.path.split(name)
        if ".." in pathlib.PurePath(dir_name).parts:
            raise SuspiciousFileOperation(
                "Detected path traversal attempt in '%s'" % dir_name
            )
        validate_file_name(file_name)
        file_root, file_ext = os.path.splitext(file_name)
        increase_file_root = file_root
        # If the filename already exists, generate an alternative filename
        # until it doesn't exist.
        # Truncate original name if required, so the new filename does not
        # exceed the max_length.
        while self.exists(name) or (max_length and len(name) > max_length):
            increase_file_root = increase_file_name(increase_file_root)
            # file_ext includes the dot.
            name = os.path.join(
                dir_name, increase_file_root+file_ext  # здесь поставлено увеличение значения числа в скобочках вместо добавления нескольких случайных символов
            )
            if max_length is None:
                continue
            # Truncate file_root if max_length exceeded.
            truncation = len(name) - max_length
            if truncation > 0:
                file_root = file_root[:-truncation]
                # Entire file_root was truncated in attempt to find an
                # available filename.
                if not file_root:
                    raise SuspiciousFileOperation(
                        'Storage can not find an available filename for "%s". '
                        "Please make sure that the corresponding file field "
                        'allows sufficient "max_length".' % name
                    )
                increase_file_root = increase_file_name(file_root)
                name = os.path.join(
                    dir_name, increase_file_root + file_ext  # здесь поставлено увеличение значения числа в скобочках вместо добавления нескольких случайных символов
                )
        return name

    def path(self, name: str):
        if name.find('project_files') != -1:
            name = name.replace('project_files/', '', 1)
        p = safe_join(self.location, name)
        if os.path.exists(p) or os.path.isdir(p):
            return p
        else:
            basename = os.path.basename(name)
            file_root, file_ext = os.path.splitext(basename)
            encoded_root = b32encode(file_root.encode()).decode()
            return safe_join(self.location, os.path.dirname(name), encoded_root+file_ext)
        
    def _open(self, name, mode="rb"):
        return File(open(self.path(name), mode), name=os.path.basename(self.get_norm_path(self.path(name))))
    
    def _save(self, name, content):
        full_path = self.path(name)

        # Create any intermediate directories that do not exist.
        directory = os.path.dirname(full_path)
        try:
            if self.directory_permissions_mode is not None:
                # Set the umask because os.makedirs() doesn't apply the "mode"
                # argument to intermediate-level directories.
                old_umask = os.umask(0o777 & ~self.directory_permissions_mode)
                try:
                    os.makedirs(
                        directory, self.directory_permissions_mode, exist_ok=True
                    )
                finally:
                    os.umask(old_umask)
            else:
                os.makedirs(directory, exist_ok=True)
        except FileExistsError:
            raise FileExistsError("%s exists and is not a directory." % directory)

        # There's a potential race condition between get_available_name and
        # saving the file; it's possible that two threads might return the
        # same name, at which point all sorts of fun happens. So we need to
        # try to create the file, but if it already exists we have to go back
        # to get_available_name() and try again.

        while True:
            try:
                # This file has a file path that we can move.
                if hasattr(content, "temporary_file_path"):
                    file_move_safe(content.temporary_file_path(), full_path)

                # This is a normal uploadedfile that we can stream.
                else:
                    # The current umask value is masked out by os.open!
                    fd = os.open(full_path, self.OS_OPEN_FLAGS, 0o666)
                    _file = None
                    try:
                        locks.lock(fd, locks.LOCK_EX)
                        for chunk in content.chunks():
                            if _file is None:
                                mode = "wb" if isinstance(chunk, bytes) else "wt"
                                _file = os.fdopen(fd, mode)
                            _file.write(chunk)
                    finally:
                        locks.unlock(fd)
                        if _file is not None:
                            _file.close()
                        else:
                            os.close(fd)
            except FileExistsError:
                # A new name is needed if the file exists.
                name = self.get_available_name(name)
                full_path = self.path(name)
            else:
                # OK, the file save worked. Break out of the loop.
                break

        if self.file_permissions_mode is not None:
            os.chmod(full_path, self.file_permissions_mode)

        # Получаю путь, где имя файла декодировано из base32
        norm_full_path = self.get_norm_path(full_path)
        # Ensure the saved path is always relative to the storage root.
        name = os.path.relpath(norm_full_path, self.location)
        # Ensure the moved file has the same gid as the storage root.
        self._ensure_location_group_id(full_path)
        # Store filenames with forward slashes, even on Windows.
        return str(name).replace("\\", "/")
    
    def get_norm_path(self, path):
        if os.path.isdir(path):
            return path
        dirname, basename = os.path.split(path)
        file_root, file_ext = os.path.splitext(basename)
        try:
            decoded_root = b32decode(file_root.encode()).decode()
        except:
            decoded_root = file_root
        return safe_join(dirname, decoded_root+file_ext)
